﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using Amf4Asmx.AmfObjects;

namespace Amf4Asmx.IO.Readers
{
    internal class Amf0Reader : AmfBaseReader
    {
        List<Object> _amf0ObjectReferences = new List<Object>(5);

        public Amf0Reader(BinaryReader reader) : base(reader)
        {
        }

        protected virtual void Reset()
        {
            _amf0ObjectReferences.Clear();
        }

        #region Base Methods


        public override AmfHeader ReadHeader()
        {
            AmfHeader header = new AmfHeader();
            Reset();
            header.Name = ReadAmfString();
            header.MustUnderstand = ReadAmfBoolean();
            reader.ReadInt32(); // always -1?
            header.Content = ReadAmfContent();
            return header;
        }


        public override AmfBody ReadBody()
        {
            AmfBody body = new AmfBody();
            Reset();
            body.Target = ReadAmfString();
            body.Response = ReadAmfString();
            reader.ReadInt32(); // always -1?
            body.Content = ReadAmfContent();
            return body;
        }

        #endregion

        #region Read General Conetent

        private object ReadAmfContent()
        {
            Amf0TypeCode typeCode = (Amf0TypeCode)ReadAmfByte();
            return ReadAmfContent(typeCode);
        }

        protected virtual object ReadAmfContent(Amf0TypeCode typeCode)
        {
            switch (typeCode)
            {
                case Amf0TypeCode.Number:
                    return reader.ReadDouble();
                case Amf0TypeCode.Boolean:
                    return reader.ReadBoolean();
                case Amf0TypeCode.String:
                    return ReadAmfString();
                case Amf0TypeCode.ASObject:
                    return ReadUnnamedDictionary();
                case Amf0TypeCode.Null:
                    return null;
                case Amf0TypeCode.Undefined:
                    return AmfUndefined.Instance;
                case Amf0TypeCode.Reference:
                    return ReadAmf0Reference();
                case Amf0TypeCode.AssociativeArray:
                    return ReadAmfAssociativeArray();
                case Amf0TypeCode.Array:
                    return ReadAmfArray();
                case Amf0TypeCode.DateTime:
                    return ReadAmfDateTime();
                case Amf0TypeCode.LongString:
                    return ReadAmfLongString();
                case Amf0TypeCode.Xml:
                    return ReadAmfXml();
                case Amf0TypeCode.CustomClass:
                    return ReadNamedDictionary();
                default:
                    throw new InvalidOperationException();
            }
        }

        #endregion

        #region Read Objects

        private void ReadAmfAssociativeArray(Dictionary<string, object> dictionary)
        {

            string key = ReadAmfString();
            Amf0TypeCode typeCode = (Amf0TypeCode)reader.ReadByte();
            while (typeCode != Amf0TypeCode.EndOfObject)
            {
                dictionary[key] = ReadAmfContent(typeCode);
                key = ReadAmfString();
                typeCode = (Amf0TypeCode)reader.ReadByte();
            }
        }

        private void AddAmf0ObjectReference(object result)
        {
            _amf0ObjectReferences.Add(result);
        }

        private object ReadAmf0Reference()
        {
            int reference = reader.ReadUInt16();
            return _amf0ObjectReferences[reference];
        }

        private object ReadAmfXml()
        {
            string text = ReadAmfLongString();
            System.Xml.XmlDocument document = new System.Xml.XmlDocument();
            if (!string.IsNullOrEmpty(text))
            {
                document.LoadXml(text);
            }
            return document;
        }

        private object ReadAmfArray()
        {
            int length = reader.ReadInt32();
            object[] result = new object[length];
            for (int i = 0; i < length; i++)
            {
                result[i] = ReadAmfContent();
            }
            AddAmf0ObjectReference(result);
            return result;
        }

        private object ReadAmfAssociativeArray()
        {
            int length = reader.ReadInt32();
            Dictionary<string, object> result = new Dictionary<string, object>(length);
            ReadAmfAssociativeArray(result);
            AddAmf0ObjectReference(result);
            return result;
        }

        private NamedDictionary ReadUnnamedDictionary()
        {
            NamedDictionary result = new NamedDictionary();
            result.TypeName = null;
            ReadAmfAssociativeArray(result.Properties);
            AddAmf0ObjectReference(result);
            return result;
        }

        private NamedDictionary ReadNamedDictionary()
        {
            NamedDictionary result = new NamedDictionary();
            result.TypeName = ReadAmfString();
            ReadAmfAssociativeArray(result.Properties);
            AddAmf0ObjectReference(result);
            return result;
        }

        private DateTime ReadAmfDateTime()
        {
            // double - milliseconds since unix epoch
            // short - time zone minutes offset
            double milliseconds = reader.ReadDouble();
            DateTime start = new DateTime(1970, 1, 1);

            DateTime date = start.AddMilliseconds(milliseconds);
            date = DateTime.SpecifyKind(date, DateTimeKind.Utc);
            short tz = reader.ReadInt16();

            date = date.AddMinutes(tz);
            date = DateTime.SpecifyKind(date, DateTimeKind.Unspecified);

            return date;
        }

        #endregion

        #region Primitives

        private string ReadAmfString()
        {
            //Get the length of the string (first 2 bytes).
            int length = reader.ReadUInt16();
            return ReadUTF(length);
        }

        protected byte ReadAmfByte()
        {
            return reader.ReadByte();
        }

        private string ReadAmfLongString()
        {
            //Get the length of the string (first 2 bytes).
            int length = reader.ReadInt32();
            return ReadUTF(length);
        }

        private bool ReadAmfBoolean()
        {
            return reader.ReadBoolean();
        }

        /// <summary>
        /// Reads an UTF-8 encoded String.
        /// </summary>
        /// <param name="length">Byte-length header.</param>
        /// <returns>The String value.</returns>
        public string ReadUTF(int length)
        {
            if (length == 0)
            {
                return string.Empty;
            }
            UTF8Encoding utf8 = new UTF8Encoding(false, true);
            byte[] encodedBytes = reader.ReadBytes(length);
            return utf8.GetString(encodedBytes, 0, encodedBytes.Length);
        }

        #endregion
    }
}
